﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace PerfTestLib.Utility
{
    /// <summary>
    /// Flatten object.
    /// </summary>
    public static class FlattenObject
    {
        public static List<List<FlattenObjectField>> FlattenTable(object obj)
        {
            List<List<FlattenObjectField>> table = new List<List<FlattenObjectField>>();

            AddObjectProperties(null, obj, table);

            return table;
        }

        private static bool IsObjectTypeArray(Type type)
        {
            return typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string);
        }

        private static void AddObjectProperties(string parentPropName, object obj, List<List<FlattenObjectField>> table)
        {
            if (obj == null)
                return;

            Type type = obj.GetType();

            if (IsObjectTypeArray(type))
            {
                AddEnumerableObjectProperties(parentPropName, obj as IEnumerable, table);
            }
            else
            {
                // get object properties
                var props = type.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance);

                // defer adding of enumerable properties
                List<PropertyInfo> enumerableProperties = new List<PropertyInfo>();

                // add object field to table
                foreach (PropertyInfo prop in props)
                {
                    if (IsObjectTypeArray(prop.PropertyType))
                    {
                        enumerableProperties.Add(prop);
                    }
                    else
                    {
                        AddObjectProperty(parentPropName, prop, obj, table);
                    }
                }

                // process enumerable object properties
                foreach (PropertyInfo prop in enumerableProperties)
                {
                    AddObjectProperty(parentPropName, prop, obj, table);
                }
            }
        }

        private static void AddEnumerableObjectProperties(string parentPropName, IEnumerable obj, List<List<FlattenObjectField>> table)
        {
            List<List<FlattenObjectField>> arrayCtxTable = new List<List<FlattenObjectField>>();

            foreach (var item in obj)
            {
                // temporary table for solving each item individually
                List<List<FlattenObjectField>> ctxTable = new List<List<FlattenObjectField>>();

                // add item properties
                AddObjectProperties(parentPropName, item, ctxTable);

                // merge item table with array table
                arrayCtxTable.AddRange(ctxTable);
            }

            // create new set of rows
            var newRows = (from ctxRow in arrayCtxTable
                           let unionRow = from row in table
                                          select row.Union(ctxRow).ToList()
                           select unionRow).SelectMany(el => el).ToList();

            // clear existing rows
            table.Clear();

            // add new rows
            table.AddRange(newRows);
        }

        private static void AddObjectProperty(string parentPropName, PropertyInfo prop, object obj, List<List<FlattenObjectField>> table)
        {
            // determine property name
            if (parentPropName == null)
            {
                parentPropName = string.Empty;
            }
            else
            {
                parentPropName = parentPropName + "_";
            }

            // get property value
            object val = obj == null ? null : prop.GetValue(obj);

            // create new property name
            string newPropName = parentPropName + prop.Name;

            if (val != null && val.GetType().IsValueType == false && val.GetType() != typeof(string))
            {
                AddObjectProperties(newPropName, val, table);
            }
            else
            {
                // add property to table
                if (table.Count == 0)
                {
                    // if table is empty then add property
                    table.Add(new List<FlattenObjectField> { new FlattenObjectField(newPropName, val) });
                }
                else
                {
                    // flatten
                    foreach (List<FlattenObjectField> row in table)
                    {
                        row.Add(new FlattenObjectField(newPropName, val));
                    }
                }
            }
        }
    }
}
